热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

层面|时期_Java高并发处理优惠券防超卖接口!

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java高并发处理优惠券防超卖接口!相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java高并发处理优惠券防超卖接口!相关的知识,希望对你有一定的参考价值。



问题抛出

在近期的项目里面有一个功能是领取优惠券的功能。

问题描述:

每一个优惠券一共发行多少张,每个用户可以领取多少张:

如:A优惠券一共发行120张,每一个用户可以领取140张,当一个用户领取优惠券成功的时候,把领取的记录写入到另外一个表中(这张表我们暂且称为表B)

Java高并发处理优惠券防超卖接口!_java

Java高并发处理优惠券防超卖接口!_sql_02


<update id="reduceStock">
update coupon set stock = stock - 1 where id = #coupon_id
update>

上面的代码按照我们的逻辑是没有问题,我通过使用PostMan软件测试也是没有问题,但是上面的代码确实是有问题的。

往往我们写的一些业务功能,在低并发的时候很多的问题会体现不出来。所以这个领取优惠券的功能我通过Jmeter软件来进行压测。

Java高并发处理优惠券防超卖接口!_redis_03

这里配置了一下,大概会发送500次请求,那来验证下优惠券会不会出现超发的问题

Java高并发处理优惠券防超卖接口!_java_04

执行结果,里面没有出现异常什么的,样本为500。包括在汇总报告里面也出现了一些返回信息是优惠券不足的信息,那来看下数据库里面的优惠券的总发行数量有没有变成负数呢?也就是有没有超发。

Java高并发处理优惠券防超卖接口!_redis_05

在测试的时候是测试的id为19的这条数据,测试完之后这里的总发行数量(stock)居然变成了-1(也就是超发了一张)。

问题引发

在解决这个问题之前,先来看下这个问题是如何引发出来的。

Java高并发处理优惠券防超卖接口!_sql_06


上面这张图是整个领取优惠券的流程(上图并没有使用流程图来画,我觉的这样画可能表达更清楚一些),在蓝色的框那里就是出现超扣减库存的时候。为啥这样说呢?

如果同时来了两个线程(你可以理解成是两个请求),比如先来的那个请求通过了检查(线程A),这时线程A还没有扣减库存,这时线程B经过一翻操作也通过了这个检查优惠券是否可领取的方法,然后线程A和线程B依次扣减库存或者是同时扣减库存,这样就会出现优惠券超领的情况。

Java高并发处理优惠券防超卖接口!_redis_07

清楚了问题引发的原因,那就来看看如何解决它们。

推荐一个开源免费的 Spring Boot 最全教程:

​​https://github.com/javastacks/spring-boot-best-practice​​

解决方案一(Java代码加锁)

在引起超发原因的那张图内可以看出,导致这一问题的根本原因是多个线程同时访问这个领取优惠券的方法,那只要保证在同一段只有一个线程进入到这个方法就可以了。

上面贴的代码就可以改成下面这样:

synchronized (this)
LoginUser loginUser = LoginInterceptor.threadLocal.get();
CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>() .eq("id", couponId) .eq("category", categoryEnum.name()));
if(couponDO == null)
throw new BizException(BizCodeEnum.COUPON_NO_EXITS);

this.checkCoupon(couponDO,loginUser.getId());
//构建领券记录
CouponRecordDO couponRecordDO = new CouponRecordDO();
BeanUtils.copyProperties(couponDO,couponRecordDO);
couponRecordDO.setCreateTime(new Date());
couponRecordDO.setUseState(CouponStateEnum.NEW.name());
couponRecordDO.setUserId(loginUser.getId());
couponRecordDO.setUserName(loginUser.getName());
couponRecordDO.setCouponId(couponDO.getId());
couponRecordDO.setId(null);
int row = couponMapper.reduceStock(couponId);
if(row == 1)
couponRecordMapper.insert(couponRecordDO);
else
log.info("发送优惠券失败:,用户:",couponDO,loginUser);

Java高并发处理优惠券防超卖接口!_redis_08

这样,经过Jmeter的压测优惠券并没有出现超发的情况。

虽然这样可以解决超发的问题,但是在项目中我们不可以这样写,原因如下:

  • synchronized的作用范围是单个JVM实例,如果是集群部署系统这里的加锁你可以理解成失效
  • 在使用了synchronized加锁后,就会形成串行等待的问题,当一个线程A在领取优惠券方法内执行过久时,其它线程会等待直到线程A执行结束

解决方案二(Sql层面解决超发)

<update id="reduceStock">
update coupon set stock = stock - 1 where id = #coupon_id and stock > 0
update>

mysql默认使用的是InnoDB引擎,使用InnoDB时在修改某一个记录的时候会将这条记录上锁,所以这个修改数据时不会出现多个线程同时修改数据。这样也可以避免优惠券超发。

如果在业务中只要有库存就可以发放优惠券的可以使用上面这种方式。

还有一种Sql的方式,可以将stock自身做为乐观锁。

<update id="reduceStock">
update product set stock=stock-1 where stock=#上一次的库存 and id = 1 and stock>0
update>

上面这种方式会存在ABA的问题,当然如果业务不在意ABA问题可以使用上面的sql,不过性能可能差一点,如果stock不匹配,这条sql也就失效了。

如果业务在意ABA问题的话也可以在表中加一个version的字段,每次修改数据的时候这个字段会加1,这样就可以避免ABA问题

<update id="reduceStock">
update product set stock=stock-1,versioin = version+1 where id = 1 and stock>0 and versinotallow=#上一次的版本号
update>

上面的这三条Sql层面的代码都可以解决优惠券超发的问题,具体使用那种就根据业务来选择了。

解决方案三(通过Redis分布式锁来解决问题)

引入Redis后,当领取优惠券时会先去Redis里面去获取锁,当锁获取成功后才可以对数据库进行操作

Java高并发处理优惠券防超卖接口!_sql_09

在分布式锁中我们应该考滤如下:

  • 排他性,在分布式集群中,同一个方法,在同一个时间只能被某一台机器上的一个线程执行
  • 容错性,当一个线程上锁后,如果机器突然的宕机,如果不释放锁,此时这条数据将会被锁死
  • 还要注意锁的粒度,锁的开销
  • 满足高可用,高性能,可重入

我们可以使用Redis里面的​​setnx​​​命令来设置锁,因为​​setnx​​是原子性的操作不可被打断

Java高并发处理优惠券防超卖接口!_sql_10

当这个命令执行成功的时候会返回​​1​​​,执行失败会返回​​0​​,我们就可以通过这个特性来判断是否获取到了锁。

先看下伪代码:

String key = "lock:coupon:" + couponId;
try
if(setnx(key,"1"))
//获取到锁
//设置Key的时期时间
exp(key,30,TimeUnit.MILLISECONDS);
try
//业务逻辑
finally
del(key);

else
//获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁

这方法里面设置key的过期时间的原因是,当机器突然的宕机后,即使没有释放掉锁,他也会在一段时间后将这个锁释放,避免导致死锁。

虽然看上面的代码是没有问题的,但是它是存在一个误删除key的问题

Java高并发处理优惠券防超卖接口!_redis_11

为了避免这个问题,可以将​​setnx​​命令设置的那个值,设置成当前线程的ID,在删除的时候判断这个线程ID是不是与当前线程的Id相同就可以了。

String key = "lock:coupon:" + couponId;
String threadId = Thread.currentThread().getId();
try
if(setnx(key,threadId))
//获取到锁
//设置Key的时期时间
exp(key,30,TimeUnit.MILLISECONDS);
try
//业务逻辑
finally
if(get(key) == threadId)
del(key);


else
//获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁

通过上面这种方法就可以解决误删除key的问题。

在finally中的这个判断和删除key的代码不是原子性的,我们可以通过lua脚本的方式来实现它们之间的原子性,将删除key的代码修改成如下:

String script = "if redis.call(get,KEYS[1]) == ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), threadId);

这里的threadId其实也可以不用,写成uuid也可以,但是在上面setnx的时候,那个值也要写成uuid

但是这样还要存在一个锁自动续期的问题,你可以开一个守护线程,每隔多久给他续期一次,或者是直接将这个过期时间延长一些。

在Redis中也有一些官方推荐的分布式锁的方式。我最后是使用的这种方式。

另外,如果你近期准备面试跳槽,建议在​​Java面试库​​小程序在线刷题,涵盖 2000+ 道 Java 面试题,几乎覆盖了所有主流技术面试题。

解决方案四(使用Redis推荐的方式)

官网地址:

​​https://redis.io/docs/manual/patterns/distributed-locks/​​

​​这个有多种实现方式,比如:Golang,Java,php

​​引入Redisson包​​

<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.17.4version>
dependency>

​​配置RedissoneClient​​

@Configuration
public class AppConfig
@Value("$spring.redis.host")
private String redisHost;
@Value("$spring.redis.port")
private String redisPort;
@Bean
public RedissonClient redisson()
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
return Redisson.create(config);

​​配置好RedissonClient后,通过getLock方法获取到锁对象后,在我们的Service层中就可以通过lock和unlock来进行加锁和释放锁了,这样还是很方便的。​​

public JsonData addCoupon(long couponId, CouponCategoryEnum categoryEnum)
String key = "lock:coupon:" + couponId;
RLock rLock = redisson.getLock(key);
LoginUser loginUser = LoginInterceptor.threadLocal.get();
rLock.lock();
try
//业务逻辑
finally
rLock.unlock();

return JsonData.buildSuccess();

​​通过这种方法也可以解决优惠券超发的问题 ,这也是Rediss官网推荐的一种方式。​​

​​使用这种方式也无需关心key过期时间续期的问题,因为在Redisson一旦加锁成功,就会启动一个watch dog,你可以将它理解成一个守护线程,它默认会每隔30秒检查一下,如果当前客户端还占有这把锁,它会自动对这个锁的过期时间进行延长。​​

也可以通过下面的方法设置watch dog的检测时间间隔

Config config = new Config();
config.setLockWatchdogTimeout();

如上就是我在解决优惠券超发时的一个思路。


推荐阅读
  • MySQL中的MVVC多版本并发控制机制的应用及实现
    本文介绍了MySQL中MVCC的应用及实现机制。MVCC是一种提高并发性能的技术,通过对事务内读取的内存进行处理,避免写操作堵塞读操作的并发问题。与其他数据库系统的MVCC实现机制不尽相同,MySQL的MVCC是在undolog中实现的。通过undolog可以找回数据的历史版本,提供给用户读取或在回滚时覆盖数据页上的数据。MySQL的大多数事务型存储引擎都实现了MVCC,但各自的实现机制有所不同。 ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • Java学习笔记之使用反射+泛型构建通用DAO
    本文介绍了使用反射和泛型构建通用DAO的方法,通过减少代码冗余度来提高开发效率。通过示例说明了如何使用反射和泛型来实现对不同表的相同操作,从而避免重复编写相似的代码。该方法可以在Java学习中起到较大的帮助作用。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • 本文介绍了Oracle存储过程的基本语法和写法示例,同时还介绍了已命名的系统异常的产生原因。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
author-avatar
粪想升或_519
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有